module Pjs2 {
    export class Runtime {
        functions: { [s: string]: FunctionDefinition } = {};
        aliases: { [s: string]: string } = {};
        variables: { [s: string]: any } = { "test": "ok" };
        operators: { [s: string]: (p: any[]) => any } = {
            "+": (p: any[]) => {
                var r;;
                p.forEach((n, i) => {
                    if (i === 0) r = n;
                    else r += n;
                });
                return r;
            },
            "-": (p: any[]) => {
                var r = 0;
                p.forEach((n, i) => {
                    if (i === 0) r = n;
                    else r -= n;
                });
                return r;
            },
            "*": (p: any[]) => {
                var r;
                p.forEach((n, i) => {
                    if (i === 0) r = n;
                    else r *= n;
                });
                return r;
            },
            "/": (p: any[]) => {
                var r = 1;
                p.forEach((n, i) => {
                    if (i == 0) r = n;
                    else r /= n;
                });
                return r;
            },
            ",": (p: any[]) => p
        }
        unaryOperators: { [s: string]: (any) => any } = {};
        static constants: { [s: string]: any } = {
            "true": true,
            "false": false,
            "null": null
        }

        public GetFunction(name: string): FunctionDefinition {
            if(name == null) return null;
            name = name.toLowerCase();
            return this.functions[name] || this.GetFunction(this.aliases[name]);
        }

        public AddFunction(fn: FunctionDefinition) {
            this.functions[fn.name.toLowerCase()] = fn;
        }

        public GetVariable(name: string) {
            var v = this.variables[name];
            if (v != null) return v;
            else return Runtime.constants[name];
        }

        public SetVariable(name: string, value: any) {
            this.variables[name] = value;
        }

        public AddAlias(name: string, forFunc: string) {
            this.aliases[name.toLowerCase()] = forFunc;
        }
    }

    export class NestedRuntime extends Runtime {
        get operators(): { [s: string]: (p: any[]) => any } {
            return this.parent.operators;
        }
        constructor(private parent: Runtime) {
            super();
        }

        public GetVariable(name: string) {
            if (this.variables[name] == null)
                return super.GetVariable(name);
            return this.variables[name];
        }

        public GetFunction(name: string) {
            if (this.functions[name.toLowerCase()] == null)
                return super.GetFunction(name);
            return this.functions[name.toLowerCase()];
        }
    }

    export class FunctionDefinition {
        constructor(public doFunc: (runtime: Runtime, params: any) => any = null,
            public parameters: FuncParameterDefinition[]= null,
            public name: string = null, public sourceScript: string = null) { }

        Call(rnt: Runtime, par: any, pipeline: any = null): any {
            // map positional to named
            for (var p in par) {
                var name = <string>p;
                if ((<string>p).match(/^_pos_\d+$/)) {
                    var index = parseInt((<string>p).substr(5))
                    if (this.parameters.length > index) {
                        name = this.parameters[index].name;
                        if (par[name] != undefined) throw "wtf positional parameter " + name;
                        par[name] = par[p];
                    }
                }
                name = name.toLowerCase();
                if (par[name] == undefined)
                    par[name] = par[p];
            }

            //  convert parameters
            this.parameters.forEach(v => {
                if (par[v.name.toLowerCase()] !== undefined) {
                    var convert = this.TryConvert(par[v.name.toLowerCase()], v);
                    if (convert === undefined)
                        throw "typecheck failed for parameter " + v.name + " on function " + this.name;
                    if (convert !== par[v.name.toLowerCase()])
                        par[v.name] = par[v.name.toLowerCase()] = convert;
                }
            });

            //map value form pipeline

            var calls = pipeline == null ? [par] : this.MapPipelineValue(pipeline, par);

            var rpar = this.parameters.filter(p => p.required);
            //call
            var res = calls.map(c => {
                //chack parameters
                rpar.forEach(v => {
                    if (c[v.name.toLowerCase()] == undefined) throw "required parameter (" + v.name + ") is not here";
                });
                return this.doFunc(rnt, c)
                });
            // TODO: IEnumerable
            if (res.length == 1)
                return res[0];
            else if (res.length == 0)
                return null;
            else return res;
        }

        private MapPipelineValue(pipeline: any, par: any): Array<any> {
            var pipar = this.parameters.filter(p => p.allowPipeline && par[p.name] === undefined);
            var res = [];
            pipar.forEach(p => {
                if (p.typecheck(pipeline)  && res.length == 0) {
                    par[p.name] = par[p.name.toLowerCase()] = pipeline;
                    res.push(par);
                }
            });
            pipar.forEach(p => {
                var convert;
                if ((convert = this.TryConvert(pipeline, p)) !== undefined && res.length == 0) {
                    par[p.name] = par[p.name.toLowerCase()] = convert;
                    res.push(par);
                }
            });
            if (res.length > 0) return res;

            if (pipeline instanceof Array) {
                var calls = new Array<any>();
                var possible = (<Array<any>>pipeline).every(v => {
                    try {
                        var par2 = shallowCopy(par)
                        var r = this.MapPipelineValue(v, par2);
                        calls.push(par2);
                        return true;
                    }
                    catch (ex) {
                        return false;
                    }
                });
                if (possible)
                    return calls;
            }
            

            throw "can't map pipeline value to function " + this.name;
        }

        private TryConvert(value: any, p: FuncParameterDefinition): any {
            if (p.typecheck && !p.typecheck(value))
                return p.typeconvert(value);
            else return value;
        }

        static FromScript(script: Script, name: string, parameters: FuncParameterDefinition[]): FunctionDefinition {
            return new FunctionDefinition(
                (r, params) => {
                    var nr = new NestedRuntime(r);
                    for (var p in params) {
                        if ((<string>p).substr(0, 5) == "_pos_") {
                            var index = parseInt((<string>p).substr(5))
                            var name = parameters[index].name;
                            nr.SetVariable(name, params[p].Do? params[p].Do(r) : params[p]);
                        } else {
                            nr.SetVariable(p, params[p].Do ? params[p].Do(r) : params[p]);
                        }
                    }

                    return script.Run(nr);
                }, parameters, name, script.source);
        }
    }

    export class FuncParameterDefinition {
        constructor(
            public name: string,
            public required: boolean = false,
            public allowPipeline = true,
            //public allowPositional = true,
            public typecheck: (o: any) => boolean = o => true,
            public typeconvert: (a: any) => any = o => typecheck(o) ? o : undefined) {
        }
    }

    export class Script {
        constructor(public commands: Array<ICommand> = new Array<ICommand>(), public source = null) { }

        public Run(r: Runtime): IEnumerable<any> {
            var result = new EnumBuilder<any>();
            this.runInternal(r, 0, result);
            return result;
        }

        public runInternal(r: Runtime, index: number, result: EnumBuilder<any>) {
            if (index == this.commands.length) {
                result.end();
                return;
            }
            var cres = this.commands[index].Do(r);
            this.procResult(cres, result).OnReturn(() => this.runInternal(r, index + 1, result));
        }

        private procResult(res, builder: EnumBuilder<any>): ReturnCallback {
            if (typeof res.getEnumerator == "function") {
                return this.procResEnumerable(<IEnumerable<any>>res, builder);
            }
            else if (res instanceof ReturnCallback) {
                return ReturnCallback.ProcessOne(res, value => this.procResult(value, builder));
            }
            else if (res != null) builder.addValue(res);
            return ReturnCallback.Done(true);
        }

        private procResEnumerable(res: IEnumerable<any>, builder: EnumBuilder<any>): ReturnCallback {
            var rc = new ReturnCallback();
            return Enumerable.forEach(res, value => {
                return this.procResult(value, builder);
            });
        }

    }

    export interface ICommand {
        Do(r: Runtime): any;
    }

    export class ScriptBlockCreate implements ICommand {
        constructor(public script: Script) {
        }

        Do(r: Runtime) {
            return this.script;
        }
    }

    export class StringCreate implements ICommand {
        constructor(public value: string) {
        }

        Do(r: Runtime) {
            return this.value;
        }
    }

    export class NumberCreate implements ICommand {
        constructor(public value: number) {
        }

        Do(r: Runtime) {
            return this.value;
        }
    }
    
    export class Cast implements ICommand {
        constructor(public value: ICommand, public name: string) { }
        
        Do(r: Runtime){
            throw "casting not supported";
        }
    }

    export class MethodCall implements ICommand {
        constructor(public on: ICommand, public name: string, public parameters = new Array<ICommand>()) { }

        Do(r: Runtime): any {
            var on = this.on.Do(r)
            var f = <Function>on[this.name];
            if (f == null) {
                throw "function '" + this.name + "' was not found";
            }
            return f.apply(on, this.parameters.map(p => p.Do(r)));
        }
    }

    export class Property implements ICommand {
        constructor(public on: ICommand, public name: string) { }

        Do(r: Runtime) {
            return ReturnCallback.ProcessOne(this.on.Do(r), d => d[this.name]);
        }
    }

    export class VariableCall implements ICommand {
        constructor(public name: string) { }

        Do(r: Runtime) {
            return r.GetVariable(this.name);
        }
    }
    
    export class VariableSet implements ICommand {
        constructor(public vname: string, public content: ICommand, public typeAccelerator: string = null) { }
        
        Do(r: Runtime) {
            var cont = this.content.Do(r);
            // TODO: type accelerator implement
            r.SetVariable(this.vname, cont)
        }
    }
    
    export class PropertySet implements ICommand {
        constructor(public property: Property, public value: ICommand) { }
        
        Do(r: Runtime) {
            var on = this.property.on.Do(r);
            return ReturnCallback.ProcessOne(on, o => ReturnCallback.ProcessOne(this.value.Do(r), v => o[this.property.name] = v));
        }
    }

    export class Condition implements ICommand {
        constructor(public condition: ICommand, public ifScript: Script, public elseScript: Script = new Script()) { }

        Do(r: Runtime) {
            var rc = new ReturnCallback();
            ReturnCallback.ProcessOne(this.condition.Do(r), d => {
                if (<boolean>d) {
                    rc.Return(this.ifScript.Run(r));
                }
                else {
                    rc.Return(this.elseScript.Run(r));
                }
            });
            return rc;
        }
    }

    export class UnaryOperator implements ICommand {
        constructor(public operator: string, public command: ICommand) { }

        Do(r: Runtime) {
            throw "unary operators are not implemented";
        }
    }

    export class Operator implements ICommand {
        constructor(public operator: string, public commands: Array<ICommand>) { }

        Do(r: Runtime) {
            return ReturnCallback.Process(this.commands.map(c => c.Do(r)), d => r.operators[this.operator](d));
        }
    }

    export class Call implements ICommand {
        constructor(public name: string, public parameters: { [s: string]: ICommand } = {}, public pipeline: ICommand = null) {
        }

        Do(r: Runtime) {
            var f = r.GetFunction(this.name);
            if (f == null) throw "function '" + this.name + "' was not found";
            var parray = [];
            var pnames = [];
            for (var p in (<any>this.parameters)) {
                parray.push(this.parameters[p].Do(r));
                pnames.push(p);
            }
            var pipelineValue = this.pipeline == null? null: this.pipeline.Do(r);
            return ReturnCallback.Process(parray, arr => {
                var params = {};
                for(var i = 0; i < arr.length; i++) {
                    params[pnames[i]] = arr[i];
                }
                return ReturnCallback.ProcessOne(pipelineValue, p => 
                    f.Call(r, params, p));
            });
        }
    }

    export class StaticClass implements ICommand {
        constructor(public path: string[]) { }

        Do(r: Runtime) {
            var c = window;
            for (var i = 0, len = this.path.length; i < len; i++) {
                c = c[this.path[i]];
            }

            return c;
        }
    }

    export class DefineFunction implements ICommand {
        constructor(public definition: FunctionDefinition) { }

        Do(r: Runtime) {
            r.AddFunction(this.definition);
            return null;
        }
    }

    export class NewHashtable implements ICommand {
        constructor(public fields: Array<{ name: StringCreate; value: ICommand }>) { }

        Do(r: Runtime) {
            return ReturnCallback.Process(this.fields.map(v => v.value.Do(r)), objs => {
                return ReturnCallback.Process(this.fields.map(v => v.name.Do(r)), names => {
                    var obj: any = {};
                    objs.forEach((f, i) => {
                        obj[names[i]] = f;
                    });
                    obj.$type = "hashtable";
                    return obj;
                });
            });
        }
    }
}